﻿//==============================================================================
// File: TimePicker.cs
// Created: 2010-05-28
// Author: Piotr Włodek, Arek Świerczek
//==============================================================================
// This file is a part of MAMMOTH project.
// Copyright (C) 2010 AGH University of Science and Technology, Krakow.
// https://caribou.iisg.agh.edu.pl/trac/mammoth
//
// Mammoth is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// Mammoth is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
// 
// You should have received a copy of the GNU Lesser General Public License
// along with Mammoth. If not, see http://www.gnu.org/licenses/.
//==============================================================================

using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace WpfRcp.Controls
{
    /// <summary>
    /// Time Picker as a control that lets the user select a specific time
    /// </summary>
    [TemplatePart(Name = "PART_Days", Type = typeof(TextBox)),
     TemplatePart(Name = "PART_Hours", Type = typeof(TextBox)),
     TemplatePart(Name = "PART_Minutes", Type = typeof(TextBox)),
     TemplatePart(Name = "PART_Seconds", Type = typeof(TextBox)),
     TemplatePart(Name = "PART_IncreaseTime", Type = typeof(ButtonBase)),
     TemplatePart(Name = "PART_DecrementTime", Type = typeof(ButtonBase))]
    public class TimePicker : Control
    {
        TextBox m_Hours, m_Minutes, m_Seconds, m_Days;
        TextBox m_CurrentlySelectedTextBox;

        /// <summary>
        /// Static constructor
        /// </summary>
        static TimePicker()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(TimePicker), new FrameworkPropertyMetadata(typeof(TimePicker)));
        }

        #region Properties

        /// <summary>
        /// Gets or sets the minimum time that can be selected
        /// </summary>
        public TimeSpan MinTime
        {
            get { return (TimeSpan)GetValue(MinTimeProperty); }
            set { SetValue(MinTimeProperty, value); }
        }

        /// <summary>
        /// Gets or sets the minimum time selected
        /// </summary>
        public static readonly DependencyProperty MinTimeProperty =
            DependencyProperty.Register("MinTime", typeof(TimeSpan), typeof(TimePicker), new UIPropertyMetadata(TimeSpan.Zero,
                                                                                                                delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                                                                                                                    {
                                                                                                                        TimePicker picker = (TimePicker)sender;
                                                                                                                        picker.CoerceValue(SelectedTimeProperty);//make sure to update the time if appropiate
                                                                                                                    }));

        /// <summary>
        /// Gets or sets the maximum time that can be selected
        /// </summary>
        public TimeSpan MaxTime
        {
            get { return (TimeSpan)GetValue(MaxTimeProperty); }
            set { SetValue(MaxTimeProperty, value); }
        }

        /// <summary>
        /// Gets or sets the maximum time that can be selected
        /// </summary>
        public static readonly DependencyProperty MaxTimeProperty =
            DependencyProperty.Register("MaxTime", typeof(TimeSpan), typeof(TimePicker), new UIPropertyMetadata(TimeSpan.MaxValue,
                                                                                                                delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                                                                                                                    {
                                                                                                                        TimePicker picker = (TimePicker)sender;
                                                                                                                        picker.CoerceValue(SelectedTimeProperty);//make sure to update the time if appropiate
                                                                                                                    }));


        /// <summary>
        /// Gets or sets the selected timestamp 
        /// </summary>
        public TimeSpan SelectedTime
        {
            get { return (TimeSpan)GetValue(SelectedTimeProperty); }
            set { SetValue(SelectedTimeProperty, value); }
        }

        /// <summary>
        /// Backing store for the selected timestamp 
        /// </summary>
        public static readonly DependencyProperty SelectedTimeProperty =
            DependencyProperty.Register("SelectedTime", typeof(TimeSpan),
                                        typeof(TimePicker), new FrameworkPropertyMetadata(TimeSpan.Zero, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedTimePropertyChanged));

        private static void SelectedTimePropertyChanged(DependencyObject sender,
                                                        DependencyPropertyChangedEventArgs e)
        {
            TimePicker timePicker = (TimePicker)sender;
            TimeSpan newTime = (TimeSpan)e.NewValue;
            TimeSpan oldTime = (TimeSpan)e.OldValue;

            if (newTime < timePicker.MinTime)
                newTime = timePicker.MinTime;
            if (newTime > timePicker.MaxTime)
                newTime = timePicker.MaxTime;

            if (!timePicker.isUpdatingTime)
            {
                timePicker.BeginUpdateSelectedTime();//signal that the selected time is being updated

                timePicker.SelectedTime = newTime;

                if (timePicker.SelectedDay != newTime.Days)
                    timePicker.SelectedDay = newTime.Days;

                if (timePicker.SelectedHour != newTime.Hours)
                    timePicker.SelectedHour = newTime.Hours;

                if (timePicker.SelectedMinute != newTime.Minutes)
                    timePicker.SelectedMinute = newTime.Minutes;

                if (timePicker.SelectedSecond != newTime.Seconds)
                    timePicker.SelectedSecond = newTime.Seconds;

                timePicker.EndUpdateSelectedTime();//signal that the selected time has been updated
                timePicker.OnTimeSelectedChanged(timePicker.SelectedTime, oldTime);
            }
        }

        /// <summary>
        /// Gets or sets the selected Day
        /// </summary>
        public int SelectedDay
        {
            get { return (int)GetValue(SelectedDayProperty); }
            set { SetValue(SelectedDayProperty, value); }
        }

        /// <summary>
        /// Backing store for the selected hour
        /// </summary>
        public static readonly DependencyProperty SelectedDayProperty =
            DependencyProperty.Register("SelectedDay", typeof(int), typeof(TimePicker), new UIPropertyMetadata(0,
                                                                                                               delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                                                                                                                   {
                                                                                                                       TimePicker timePicker = (TimePicker)sender;

                                                                                                                       //set the new timespan
                                                                                                                       SetNewTime(timePicker);
                                                                                                                   }));

        /// <summary>
        /// Gets or sets the selected Hour
        /// </summary>
        public int SelectedHour
        {
            get { return (int)GetValue(SelectedHourProperty); }
            set { SetValue(SelectedHourProperty, value); }
        }

        /// <summary>
        /// Backing store for the selected hour
        /// </summary>
        public static readonly DependencyProperty SelectedHourProperty =
            DependencyProperty.Register("SelectedHour", typeof(int), typeof(TimePicker), new UIPropertyMetadata(0,
                                                                                                                delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                                                                                                                    {
                                                                                                                        TimePicker timePicker = (TimePicker)sender;

                                                                                                                        //set the new timespan
                                                                                                                        SetNewTime(timePicker);
                                                                                                                    }));

        /// <summary>
        /// Gets or sets the selected minutes
        /// </summary>
        public int SelectedMinute
        {
            get { return (int)GetValue(SelectedMinuteProperty); }
            set { SetValue(SelectedMinuteProperty, value); }
        }

        /// <summary>
        /// Backing store for the selected minsutes
        /// </summary>
        public static readonly DependencyProperty SelectedMinuteProperty =
            DependencyProperty.Register("SelectedMinute", typeof(int), typeof(TimePicker), new UIPropertyMetadata(0,
                                                                                                                  delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                                                                                                                      {
                                                                                                                          TimePicker timePicker = (TimePicker)sender;

                                                                                                                          //set the new timespan
                                                                                                                          SetNewTime(timePicker);
                                                                                                                      }));

        /// <summary>
        /// Gets or sets the selected second
        /// </summary>
        public int SelectedSecond
        {
            get { return (int)GetValue(SelectedSecondProperty); }
            set { SetValue(SelectedSecondProperty, value); }
        }

        /// <summary>
        /// Backing store for the selected second
        /// </summary>
        public static readonly DependencyProperty SelectedSecondProperty =
            DependencyProperty.Register("SelectedSecond", typeof(int), typeof(TimePicker), new UIPropertyMetadata(0,
                                                                                                                  delegate(DependencyObject sender, DependencyPropertyChangedEventArgs e)
                                                                                                                      {
                                                                                                                          TimePicker timePicker = (TimePicker)sender;

                                                                                                                          //set the new timespan
                                                                                                                          SetNewTime(timePicker);
                                                                                                                      }));

        #endregion

        #region Events

        /// <summary>
        /// 
        /// </summary>
        public static readonly RoutedEvent SelectedTimeChangedEvent = EventManager.RegisterRoutedEvent("SelectedTimeChanged",
                                                                                                       RoutingStrategy.Bubble, typeof(TimeSelectedChangedEventHandler), typeof(TimePicker));

        public event TimeSelectedChangedEventHandler SelectedTimeChanged
        {
            add { AddHandler(SelectedTimeChangedEvent, value); }
            remove { RemoveHandler(SelectedTimeChangedEvent, value); }
        }

        #endregion

        #region Overrides

        /// <summary>
        /// override to hook to the Control template elements
        /// </summary>
        public override void OnApplyTemplate()
        {
            //get the days textbox and hook the events to it
            m_Days = GetTemplateChild("PART_Days") as TextBox;
            if (m_Days != null)
            {
                m_Days.PreviewTextInput += DaysTextChanged;
                m_Days.KeyUp += DaysKeyUp;
                m_Days.GotFocus += TextGotFocus;
                m_Days.GotMouseCapture += TextGotFocus;
            }

            //get the hours textbox and hook the events to it
            m_Hours = GetTemplateChild("PART_Hours") as TextBox;
            if (m_Hours != null)
            {
                m_Hours.PreviewTextInput += HoursTextChanged;
                m_Hours.KeyUp += HoursKeyUp;
                m_Hours.GotFocus += TextGotFocus;
                m_Hours.GotMouseCapture += TextGotFocus;
            }

            //get the minutes textbox and hook the events to it
            m_Minutes = GetTemplateChild("PART_Minutes") as TextBox;
            if (m_Minutes != null)
            {
                m_Minutes.PreviewTextInput += MinutesTextChanged;
                m_Minutes.KeyUp += MinutesKeyUp;
                m_Minutes.GotFocus += TextGotFocus;
                m_Minutes.GotMouseCapture += TextGotFocus;
            }

            //get the seconds textbox and hook the events to it
            m_Seconds = GetTemplateChild("PART_Seconds") as TextBox;
            if (m_Seconds != null)
            {
                m_Seconds.PreviewTextInput += SecondsTextChanged;
                m_Seconds.KeyUp += SecondsKeyUp;
                m_Seconds.GotFocus += TextGotFocus;
                m_Seconds.GotMouseCapture += TextGotFocus;
            }

            //Get the increase button and hook to the click event
            ButtonBase increaseButton = GetTemplateChild("PART_IncreaseTime") as ButtonBase;
            if (increaseButton != null) 
                increaseButton.Click += IncreaseTime;

            //Get the decrease button and hook to the click event
            ButtonBase decrementButton = GetTemplateChild("PART_DecrementTime") as ButtonBase;
            if (decrementButton != null) 
                decrementButton.Click += DecrementTime;
        }

        #endregion

        #region Event Handlers

        //event handler for the textboxes (hours, minutes, seconds)
        private void TextGotFocus(object sender, RoutedEventArgs e)
        {
            TextBox selectedBox = (TextBox)sender;
            //set the currently selected textbox. 
            //This field is used to check which entity(hour/minute/second) to increment/decrement when user clicks the buttuns
            m_CurrentlySelectedTextBox = selectedBox;

            //highlight all code so that it is easier to the user to enter new info in the text box
            selectedBox.SelectAll();
        }

        //event handler for the Hour TextBox
        private void DaysTextChanged(object sender, TextCompositionEventArgs e)
        {
            //delete the text that is highlight(selected)
            TrimSelectedText(m_Days);

            //Adjust the text according to the carrot index
            string newText = AdjustText(m_Days, e.Text);

            //validates that the hour is correct if not set a valid value (0 or 24)
            ValidateAndSetDay(newText);

            //moves the carrot index or focus the neighbour
            AdjustCarretIndexOrMoveToNeighbour(m_Days, m_Hours);

            //handle the event so that it does not set the text, since we do it manually
            e.Handled = true;
        }

        //event handler for the Hour TextBox
        private void HoursTextChanged(object sender, TextCompositionEventArgs e)
        {
            //delete the text that is highlight(selected)
            TrimSelectedText(m_Hours);

            //Adjust the text according to the carrot index
            string newText = AdjustText(m_Hours, e.Text);

            //validates that the hour is correct if not set a valid value (0 or 24)
            ValidateAndSetHour(newText);

            //moves the carrot index or focus the neighbour
            AdjustCarretIndexOrMoveToNeighbour(m_Hours, m_Minutes);

            //handle the event so that it does not set the text, since we do it manually
            e.Handled = true;
        }

        //event handler for the Minute TextBox
        private void MinutesTextChanged(object sender, TextCompositionEventArgs e)
        {
            //delete the text that is highlight(selected)
            TrimSelectedText(m_Minutes);

            //Adjust the text according to the carrot index
            string newText = AdjustText(m_Minutes, e.Text);

            //validates that the minute is correct if not set a valid value (0 or 59)
            ValidateAndSetMinute(newText);

            //moves the carrot index or focus the neighbour
            AdjustCarretIndexOrMoveToNeighbour(m_Minutes, m_Seconds);

            //handle the event so that it does not set the text, since we do it manually
            e.Handled = true;
        }

        //event handler for the Second TextBox
        private void SecondsTextChanged(object sender, TextCompositionEventArgs e)
        {
            //delete the text that is highlight(selected)
            TrimSelectedText(m_Seconds);

            //Adjust the text according to the carrot index
            string newText = AdjustText(m_Seconds, e.Text);

            //validates that the second is correct if not set a valid value (0 or 59)
            ValidateAndSetSeconds(newText);

            //moves the carrot index or focus the neighbour
            AdjustCarretIndexOrMoveToNeighbour(m_Seconds, null);

            //handle the event so that it does not set the text, since we do it manually
            e.Handled = true;
        }

        //increments/decrement the selected time accordingly to the selected control
        private bool IncrementDecrementTime(Key selectedKey)
        {
            if (selectedKey == Key.Up)
                IncrementDecrementTime(true);
            else if (selectedKey == Key.Down)
                IncrementDecrementTime(false);
            else
                return false;
            return true;
        }

        private void DaysKeyUp(object sender, KeyEventArgs e)
        {
            //focus the next control
            TryFocusNeighbourControl(m_Days, null, m_Hours, e.Key);

            if (!IncrementDecrementTime(e.Key))
                ValidateAndSetDay(m_Days.Text);
        }

        private void HoursKeyUp(object sender, KeyEventArgs e)
        {
            //focus the next control
            TryFocusNeighbourControl(m_Hours, m_Days, m_Minutes, e.Key);

            if (!IncrementDecrementTime(e.Key))
                ValidateAndSetHour(m_Hours.Text);
        }

        private void MinutesKeyUp(object sender, KeyEventArgs e)
        {
            //focus the next control
            TryFocusNeighbourControl(m_Minutes, m_Hours, m_Seconds, e.Key);

            if (!IncrementDecrementTime(e.Key))
                ValidateAndSetMinute(m_Minutes.Text);
        }

        private void SecondsKeyUp(object sender, KeyEventArgs e)
        {
            //focus the next control
            TryFocusNeighbourControl(m_Seconds, m_Minutes, null, e.Key);

            if (!IncrementDecrementTime(e.Key))
                ValidateAndSetSeconds(m_Seconds.Text);
        }

        #endregion

        #region Increase Decrease Button Handlers

        //event handler for the decrease button click
        private void DecrementTime(object sender, RoutedEventArgs e)
        {
            IncrementDecrementTime(false);
        }

        private void IncreaseTime(object sender, RoutedEventArgs e)
        {
            IncrementDecrementTime(true);
        }

        #endregion

        #region Helper methods

        //increment or decrement the time (hour/minute/second) currently in selected (determined by the currentlySelectedTextBox that is set in the GotFocus event of the textboxes)
        private void IncrementDecrementTime(bool increment)
        {
            if (m_Days == m_CurrentlySelectedTextBox)
                SelectedDay += increment ? 1 : -1;
            else if (m_Hours == m_CurrentlySelectedTextBox)
                SelectedHour += increment ? 1 : -1;
            else if (m_Minutes == m_CurrentlySelectedTextBox)
                SelectedMinute += increment ? 1 : -1;
            else
                SelectedSecond += increment ? 1 : -1;
        }

        #region Validate and set properties
        //validates the day passed as text and sets it to the SelectedHour property
        private void ValidateAndSetDay(string text)
        {
            int dayNum = ValidateNumber(text);
            SelectedDay = dayNum;
        }

        //validates the hour passed as text and sets it to the SelectedHour property
        private void ValidateAndSetHour(string text)
        {
            int hourNum = ValidateNumber(text);
            SelectedHour = hourNum;
        }

        //validates the minute passed as text and sets it to the SelectedMinute property
        private void ValidateAndSetMinute(string text)
        {
            int minNum = ValidateNumber(text);
            SelectedMinute = minNum;
        }

        //validates the second passed as text and sets it to the SelectedSecond property
        private void ValidateAndSetSeconds(string text)
        {
            int secNum = ValidateNumber(text);
            SelectedSecond = secNum;
        }
        #endregion

        //focuses the left/right control accordingly to the key passed. Pass null if there is not a neighbour control
        private static void TryFocusNeighbourControl(TextBox currentControl, TextBox leftControl,
                                                     TextBox rightControl, Key keyPressed)
        {
            if (keyPressed == Key.Left &&
                leftControl != null &&
                currentControl.CaretIndex == 0)
            {
                leftControl.Focus();
            }

            else if (keyPressed == Key.Right &&
                     rightControl != null &&
                     //if the caret index is the same as the length of the text and the user clicks right key it means that he wants to go to the next textbox
                     currentControl.CaretIndex == currentControl.Text.Length)
            {
                rightControl.Focus();
            }
        }

        //remove the left hand side number if the carrot index is 0 if the carrot index is 1 it removes the right hand side text
        private static string AdjustText(TextBox textBox, string newText)
        {
            //replace the new text with the old text if there are already 2 char in the textbox
            if (textBox.Text.Length == 2)
            {
                if (textBox.CaretIndex == 0)
                    return newText + textBox.Text[1];
                return textBox.Text[0] + newText;
            }
            
            return textBox.CaretIndex == 0 ? newText + textBox.Text //if the carret is in front the text append the new text infront
                       : textBox.Text + newText; //else put it in behind the existing text
        }

        //moves the carrot for the textbox and if the carrot is at the end it will focus the neighbour
        private static void AdjustCarretIndexOrMoveToNeighbour(TextBox current, TextBox neighbour)
        {
            //if the current is near the end move to neighbour
            if (current.CaretIndex == 1 && neighbour != null)
                neighbour.Focus();

                //if the carrot is in the first index move the caret one index
            else if (current.CaretIndex == 0)
                current.CaretIndex++;
        }

        //Removes the selected text
        private static void TrimSelectedText(TextBox textBox)
        {
            if (textBox.SelectionLength > 0)
                textBox.Text = textBox.Text.Remove(textBox.SelectionStart, textBox.SelectionLength);
        }

        //sets the selectedTime with the selectedhour, selectedminute and selectedsecond
        private static void SetNewTime(TimePicker timePicker)
        {
            if (!timePicker.isUpdatingTime)
            {
                var newTime = new TimeSpan(timePicker.SelectedDay, timePicker.SelectedHour, timePicker.SelectedMinute, timePicker.SelectedSecond);

                //check if the time is the same
                if (timePicker.SelectedTime != newTime)
                    timePicker.SelectedTime = newTime;
            }
        }

        private bool isUpdatingTime;
        //call this method while updating the SelectedTimeProperty from the control itself only
        private void BeginUpdateSelectedTime()
        {
            isUpdatingTime = true;
        }

        private void EndUpdateSelectedTime()
        {
            isUpdatingTime = false;
        }

        private void OnTimeSelectedChanged(TimeSpan newTime, TimeSpan oldTime)
        {
            var args = new TimeSelectedChangedRoutedEventArgs(SelectedTimeChangedEvent)
                           {
                               NewTime = newTime,
                               OldTime = oldTime
                           };
            RaiseEvent(args);
        }

        public int ValidateNumber(string newNum)
        {
            int num;
            if (!int.TryParse(newNum, out num))
                return 0;

            return num;
        }

        #endregion
    }

    /// <summary>
    /// Delegate for the TimeSelectedChanged event
    /// </summary>
    /// <param name="sender">The object raising the event</param>
    /// <param name="e">The routed event arguments</param>
    public delegate void TimeSelectedChangedEventHandler(object sender, TimeSelectedChangedRoutedEventArgs e);

    /// <summary>
    /// Routed event arguments for the TimeSelectedChanged event
    /// </summary>
    public class TimeSelectedChangedRoutedEventArgs : RoutedEventArgs
    {
        /// <summary>
        /// Gets or sets the new time
        /// </summary>
        public TimeSpan NewTime { get; set; }

        /// <summary>
        /// Gets or sets the old time
        /// </summary>
        public TimeSpan OldTime { get; set; }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="routedEvent">The event that is raised </param>
        public TimeSelectedChangedRoutedEventArgs(RoutedEvent routedEvent)
            : base(routedEvent) { }
    }
}